package jmine.tec.web.servlet.filter;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

import jmine.tec.utils.debug.TimerGroup;
import jmine.tec.utils.debug.impl.TimerImpl;
import jmine.tec.utils.io.IOUtils;
import jmine.tec.utils.io.ReversibleOutputStream;
import net.sf.ehcache.constructs.web.filter.FilterServletOutputStream;

/**
 * Filter that measures servlet processing time and io time
 * 
 * @author takeshi
 */
public class TimingFilter implements Filter {

    private static final int MAX_STORED_RESULTS = 150;

    private static final int LIMIT = 2 * MAX_STORED_RESULTS;

    private final TimerGroup serviceTimer = new TimerImpl("filter service time");

    private final TimerGroup ioTime = new TimerImpl("filter io time");

    private final Map<Thread, String> openRequests = new ConcurrentHashMap<Thread, String>();

    private final AtomicLong totalBytesTransfered = new AtomicLong();

    /**
     * {@inheritDoc}
     */
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    /**
     * {@inheritDoc}
     */
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        String requestURI = httpRequest.getRequestURI();
        BufferedHttpServletResponse wrapper = new BufferedHttpServletResponse(httpResponse, this.ioTime, requestURI);
        Thread currentThread = Thread.currentThread();
        this.openRequests.put(currentThread, requestURI);
        this.serviceTimer.start(requestURI);
        try {
            chain.doFilter(httpRequest, wrapper);
        } finally {
            this.serviceTimer.stop(requestURI);
            this.openRequests.remove(currentThread);
            this.totalBytesTransfered.addAndGet(wrapper.doWriteResponse());
        }
        if (this.serviceTimer.getNumberOfTimers() > LIMIT) {
            this.serviceTimer.purgeIfGreaterThan(MAX_STORED_RESULTS);
            this.ioTime.purgeIfGreaterThan(MAX_STORED_RESULTS);
        }
    }

    /**
     * @return the total bytes that were sent
     */
    public long getTotalBytesTransfered() {
        return this.totalBytesTransfered.get();
    }

    /**
     * {@inheritDoc}
     */
    public void destroy() {
    }

    /**
     * ResponseWrapper
     * 
     * @author takeshi
     */
    private static final class BufferedHttpServletResponse extends HttpServletResponseWrapper {

        private PrintWriter printWriter;

        private ReversibleOutputStream reversibleOutputStream;

        private final TimerGroup timer;

        private final String requestURI;

        /**
         * C'tor
         * 
         * @param response {@link HttpServletResponse}
         * @param timer {@link TimerGroup}
         * @param requestURI {@link String}
         */
        public BufferedHttpServletResponse(HttpServletResponse response, TimerGroup timer, String requestURI) {
            super(response);
            this.timer = timer;
            this.requestURI = requestURI;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public PrintWriter getWriter() throws IOException {
            if (this.printWriter == null) {
                this.printWriter = new PrintWriter(this.getOutputStream());
            }
            return this.printWriter;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public ServletOutputStream getOutputStream() throws IOException {
            this.reversibleOutputStream = new ReversibleOutputStream();
            FilterServletOutputStream out = new FilterServletOutputStream(this.reversibleOutputStream);
            return out;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void flushBuffer() throws IOException {
        }

        /**
         * Actually writes the data into the outputStream
         * 
         * @return long the amount of bytes actually written
         * @throws IOException e
         */
        public long doWriteResponse() throws IOException {
            if (this.printWriter != null) {
                this.printWriter.flush();
            }
            if (this.reversibleOutputStream != null) {
                InputStream inputStream = this.reversibleOutputStream.reverse();
                this.timer.start(this.requestURI);
                try {
                    return IOUtils.copyFullyGetTotal(inputStream, super.getOutputStream());
                } finally {
                    this.timer.stop(this.requestURI);
                    this.reversibleOutputStream.close();
                    inputStream.close();
                    super.flushBuffer();
                }
            }
            return 0;
        }

    }

    /**
     * @return the serviceTimer
     */
    public TimerGroup getServiceTimer() {
        return this.serviceTimer;
    }

    /**
     * @return the ioTime
     */
    public TimerGroup getIoTimer() {
        return this.ioTime;
    }

    /**
     * @return the openRequests
     */
    public Map<Thread, String> getOpenRequests() {
        return new HashMap<Thread, String>(this.openRequests);
    }

}
